/* -*-c++-*- */
/* osgEarth - Geospatial SDK for OpenSceneGraph
 * Copyright 2008-2016 Pelican Mapping
 * http://osgearth.org
 *
 * osgEarth is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */
#ifndef OSGEARTH_ELEVATION_POOL_H
#define OSGEARTH_ELEVATION_POOL_H

#include <osgEarth/Common>
#include <osgEarth/Elevation>
#include <osgEarth/ElevationLayer>
#include <osgEarth/GeoData>
#include <osgEarth/TileKey>
#include <osgEarth/Threading>
#include <osgEarth/Containers>
#include <osgEarth/MapCallback>
#include <osg/Timer>
#include <unordered_map>
#include <queue>
#include <atomic>

namespace osgEarth
{
    /**
     * Stores pointers to elevation data wherever it might exist
     * so we can quickly access it for queries.
     */
    class OSGEARTH_EXPORT ElevationPool : public osg::Referenced
    {
    public:
        typedef osg::observer_ptr<ElevationTexture> WeakPointer;
        typedef osg::ref_ptr<ElevationTexture> Pointer;
        typedef std::unordered_map<Internal::RevElevationKey, WeakPointer> WeakLUT;

    private:
        struct OSGEARTH_EXPORT StrongLRU {
            StrongLRU(unsigned maxSize=64u);
            Mutexed<std::queue<Pointer>> _lru;
            unsigned _maxSize;
            void push(Pointer& p);
            void clear();
        };

    public:
        //! User data that a client can use to speed up queries in
        //! a local geographic area or sample a custom set of layers.
        class OSGEARTH_EXPORT WorkingSet
        {
        public:
            //! Construct a new local working set cache.
            //! @param size Cache size
            WorkingSet(unsigned size =32u);

            //! Assign a specific set of elevation layer to use 
            //! for sampling. Usually this is unnecessary as the Pool
            //! will synchronize with the map set by setMap, but you
            //! may want to use a custom Pool with a specific subset
            //! of query layers.
            //! @param layers Set of elevation layers to use
            void setElevationLayers(const ElevationLayerVector& layers) {
                _elevationLayers = layers;
            }

            //! Invalidate the cache.
            void clear();

        private:
            StrongLRU _lru;
            ElevationLayerVector _elevationLayers;
            friend class ElevationPool;
        };

        /**
         * Object for doing lots of queries in a localized area.
         * Call newEnvelope to initialize one.
         */
        class OSGEARTH_EXPORT Envelope
        {
        public:
            int sampleMapCoords(
                std::vector<osg::Vec3d>& points,
                ProgressCallback* progress);

        public:
            using QuickCache = vector_map<
                Internal::RevElevationKey,
                osg::ref_ptr<ElevationTexture>>;

            struct QuickSampleVars {
                double sizeS, sizeT;
                double s, t;
                double s0, s1, smix;
                double t0, t1, tmix;
                osg::Vec4f UL, UR, LL, LR, TOP, BOT;
            };

        private:
            Internal::RevElevationKey _key;
            QuickCache _cache;
            QuickSampleVars _vars;
            double _pw, _ph, _pxmin, _pymin;
            osg::ref_ptr<ElevationTexture> _raster;
            int _lod;
            unsigned _tw, _th;
            WorkingSet* _ws;
            WorkingSet _default_ws;
            osg::ref_ptr<const Map> _map;
            osg::ref_ptr<const Profile> _profile;
            ElevationPool* _pool;

            friend class ElevationPool;
        };

    public:
        //! Construct the elevation pool
        ElevationPool();

        //! Assign map to the pool. Required.
        void setMap(const Map* map);

        //! Sample the map's elevation at a point.
        //! @param p  Point at which to sample
        //! @param ws Optional working set (can be NULL)
        //! @param progress Optional progress callback
        ElevationSample getSample(
            const GeoPoint& p,
            WorkingSet* ws,
            ProgressCallback* progress =nullptr);

        //! Sample the map's elevation at a point.
        //! @param p Point at which to sample
        //! @param resolution Resolution at which to attempt to sample (in point srs)
        //! @param ws Optional working set (can be NULL)
        //! @param progress Optional progress callback
        ElevationSample getSample(
            const GeoPoint& p, 
            const Distance& resolution,
            WorkingSet* ws,
            ProgressCallback* progress =nullptr);

        //! Extract a complete tile of elevation data
        //! @param key TileKey or data to extact
        //! @param getResolutions Whether to include the resolution of each tile sample
        //! @param acceptLowerRes Return a lower resolution tile if the requested one isn't available
        //! @param out_elev Output
        //! @param ws Optional working set
        //! @param progress Optional progress callback
        //! @return true upon success, false upon failure
        bool getTile(
            const TileKey& key,
            bool acceptLowerRes,
            osg::ref_ptr<ElevationTexture>& out_elev,
            WorkingSet* ws,
            ProgressCallback* progress);

        //! For each point in an array of points, sample the elevation and store
        //! the result in the Z coordinate. Input points must be in the map's SRS,
        //! and the sampling resolution is taken from the W coordinate.
        //! @param points Array of points in map coords for which to sample elevation
        //! @param ws Optional working set (local cache)
        //! @param progress Optional progress callback
        //! @return Number of valid elevations sampled, or -1 if there was an error
        int sampleMapCoords(
            std::vector<osg::Vec4d>& points,
            WorkingSet* ws,
            ProgressCallback* progress);

        //! For each point in an array of points, sample the elevation and store
        //! the result in the Z coordinate. Input points must be in the map's SRS.
        //! @param points Array of points in map coords for which to sample elevation
        //! @param resolution Resolution at which to sample the points
        //! @param ws Optional working set (local cache)
        //! @param progress Optional progress callback
        //! @return Number of valid elevations sampled, or -1 if there was an error
        int sampleMapCoords(
            std::vector<osg::Vec3d>& points,
            const Distance& resolution,
            WorkingSet* ws,
            ProgressCallback* progress);;

        //! Creates an envelope for sampling lots of points in a localized region
        bool prepareEnvelope(
            Envelope& out,
            const GeoPoint& refPoint,
            const Distance& resolution,
            WorkingSet* ws =nullptr);

        //! For each point in an array of points, sample the elevation and store
        //! the result in the Z coordinate. Input points must be in the map's SRS.
        //! @param points Array of points in map coords for which to sample elevation
        //! @param session Session created by calling beginSession
        //! @param progress Optional progress callback
        //! @return Number of valid elevations sampled, or -1 if there was an error
        //int sampleMapCoords(
        //    std::vector<osg::Vec3d>& points,
        //    SampleSession& session,
        //    ProgressCallback* progress);

        //! Invalidates all caches in the ElevationPool
        void clear();

        //! Destructor
        virtual ~ElevationPool();

    private:

        // weak pointer to the map from whence this pool came
        osg::observer_ptr<const Map> _map;

        // stores weak pointers to elevation textures wherever they may exist
        // elsewhere in the system, including the local L2 LRU.
        WeakLUT _globalLUT;
        Threading::ReadWriteMutex _globalLUTMutex;

        // LRU container that stores the last N strong references to accessed tiles.
        // Not used directly - just used to hold ref_ptrs to things so they stay
        // alive in the global LUT (see above).
        StrongLRU _L2;

        // internal: spatial index of data extents
        void* _index;

        // elevation tile size
        unsigned _tileSize;

        //WorkingSet* _L2;

        bool _mapDataDirty;

        Threading::Mutex _refreshMutex;

        struct MapCallbackAdapter : public MapCallback {
            void onMapModelChanged(const MapModelChange&);
            ElevationPool* _pool;
        };
        friend struct MapCallbackAdapter; 

        osg::ref_ptr<MapCallbackAdapter> _mapCallback;

        ElevationLayerVector _elevationLayers;
        
        std::atomic<int> _workers;

        int getElevationRevision(const Map* map) const;

        void sync(const Map*, WorkingSet*);

        void refresh(const Map*);

        ElevationSample getSample(
            const GeoPoint& p, 
            unsigned maxLOD, 
            const Map* map, 
            WorkingSet* ws,
            ProgressCallback* progress);

        //! Best LOD this a point, or -1 if no data in index
        int getLOD(double x, double y) const;

        osg::ref_ptr<ElevationTexture> getOrCreateRaster(
            const Internal::RevElevationKey& key, 
            const Map* map, 
            bool acceptLowerRes,
            WorkingSet* ws,
            ProgressCallback* progress);

        bool findExistingRaster(
            const Internal::RevElevationKey& key,
            WorkingSet* ws,
            osg::ref_ptr<ElevationTexture>& result,
            bool* fromWorkingSet,
            bool* fromL2Cache,
            bool* fromGlobalWeakLUT);
    };

    /**
    * Utility to run elevation queries in the background.
    */
    class OSGEARTH_EXPORT AsyncElevationSampler
    {
    public:
        //! Construct a new sampler
        //! @param map Map the sampler will use to sample elevation data
        //! @param threads Number of threads the sampler should use
        AsyncElevationSampler(
            const Map* map,
            unsigned threads =1u);

        //! Destructor
        virtual ~AsyncElevationSampler() { }

        //! Sample elevation at a point at highest available resolution
        //! @param p Point at which to sample terrain elevation
        //! @return Future result of the sample
        Future<ElevationSample> getSample(
            const GeoPoint& p);

        //! Sample elevation at a point and a target resolution
        //! @param p Point at which to sample terrain elevation
        //! @param resolution Resolution at which to sample terrain
        //! @return Future result of the sample
        Future<ElevationSample> getSample(
            const GeoPoint& p,
            const Distance& resolution);

    protected:

        osg::observer_ptr<const Map> _map;
        ElevationPool::WorkingSet _ws;
        JobArena _arena;
    };
} // namespace

#endif // OSGEARTH_ELEVATION_POOL_H
